using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using System.IO;

namespace jumping_on_blocks
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Effect superEffective;

        Camera camera;
        MouseState originalMouseState;

        //TEST STUFF WITH TESTES!
        ManlyModel block;
        List<Block> blocks;

        Level currentLevel;
        int currentIndex;
        Player player;
        public static Boolean cl = true;
        Boolean tcl;
        SpriteFont font;

        Boolean gameEnded;

        Texture2D titleScreen;

        int mouseUp = -1;
        Boolean changeUp = true;

        Boolean gameStarted = false;

        List<Level> allLevels;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 800;
            graphics.IsFullScreen = false;
            graphics.ApplyChanges();
            Window.Title = "Jumping on Blocks";
            blocks = new List<Block>();
            camera = new Camera(GraphicsDevice);
            allLevels = new List<Level>();
            currentIndex = 0;
            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            superEffective = Content.Load<Effect>("HLSL-ForTheSoul");
            //setUpEffects();

            titleScreen = Content.Load<Texture2D>(@"Textures\TitleScreen");
            font = Content.Load<SpriteFont>(@"Fonts\Miramonte");

            int i = 1;
            while (true)
            {
                Level newLevel = LoadLevelFromFile(@"level" + i + ".fas");
                if (newLevel.blockSize > 0)
                {
                    newLevel.id = i;
                    allLevels.Add(newLevel);
                }
                else
                {
                    break;
                }

                i++;               
            }

            Console.WriteLine("first: " + allLevels[0].xBlocks);
            //TEST
            //Create a box Model
            block = new ManlyModel(Content.Load<Model>(@"boxen"), superEffective);
            currentLevel = allLevels.First();
            SetCurrentLevel(currentLevel);
            //END_TEST

            Mouse.SetPosition(GraphicsDevice.Viewport.Width / 2, GraphicsDevice.Viewport.Height / 2);
            originalMouseState = Mouse.GetState();

            // TODO: use this.Content to load your game content here
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            if (gameStarted)
            {
                float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f;
                ProcessInput(timeDifference);
                player.Update();
                camera.Update();

                if (player.Pos.Y < -10)
                {
                    currentIndex++;
                    if (currentIndex >= allLevels.Count)
                    {
                        //currentIndex = 0;
                        gameEnded = true;
                        gameStarted = false;
                        return;
                    }
                    currentLevel = allLevels[currentIndex];
                    blocks = new List<Block>();
                    player.free = false;
                    SetCurrentLevel(currentLevel);
                }
            }
            else if (!gameEnded)
            {
                if (Keyboard.GetState().IsKeyDown(Keys.Enter))
                {
                    gameStarted = true;
                }
            }

            if (Keyboard.GetState().IsKeyDown(Keys.Escape))
                this.Exit();

            base.Update(gameTime);
        }

        private void ProcessInput(float amount)
        {
            MouseState currentMouseState = Mouse.GetState();
            if (currentMouseState != originalMouseState)
            {
                float xDifference = currentMouseState.X - originalMouseState.X;
                float yDifference = mouseUp * (currentMouseState.Y - originalMouseState.Y);
                player.leftrightRot -= player.rotationSpeed * xDifference * amount;
                player.updownRot -= player.rotationSpeed * yDifference * amount;
                Mouse.SetPosition(graphics.GraphicsDevice.Viewport.Width / 2, graphics.GraphicsDevice.Viewport.Height / 2);
            }

            Vector3 moveVector = Vector3.Zero;
            KeyboardState keyState = Keyboard.GetState();

            if (keyState.IsKeyDown(Keys.I))
            {
                changeUp = true;
            }
            else if (changeUp)
            {
                changeUp = false;
                mouseUp = -mouseUp;
            }

            if (keyState.IsKeyDown(Keys.T))
            {
                tcl = true;
            }
            else if (tcl)
            {
                tcl = false;
                cl = !cl;
            }

            if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W))
                moveVector += Vector3.Forward;    
            if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S))
                moveVector += Vector3.Backward;
            if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D))
                moveVector += Vector3.Right;
            if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A))
                moveVector += Vector3.Left;


            if (keyState.IsKeyDown(Keys.Space) && !player.isJumping)
            {
                moveVector += Vector3.Up * 3;
                player.isJumping = true;
            }
            

            Vector3 realVector = player.TransformVelocity(moveVector);

            Vector3 legalDirections = Vector3.One;

            if (cl)
                foreach (Block block in blocks)
                {
                    if (block.boundingBox.Contains(player.Look(realVector,new Vector3(1, 0, 0))) != ContainmentType.Disjoint)
                        legalDirections.X = 0;

                    if (block.boundingBox.Contains(player.Look(realVector, new Vector3(0, 1, 0))) != ContainmentType.Disjoint)
                    {
                        legalDirections.Y = 0;
                        player.free = true;
                    }

                    if (block.boundingBox.Contains(player.Look(realVector, new Vector3(0, 0, 1))) != ContainmentType.Disjoint)
                        legalDirections.Z = 0;
                }
            else
                player.free = true;

            if (!player.free)
            {
                legalDirections.X = 0;
                legalDirections.Z = 0;
            }
            player.Move(moveVector, legalDirections, cl);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);

            //Set up vertex and pixel shaders for drawing. Yes, there are probably more passes in this effect.

            if (gameStarted)
                DrawLevel();
            else if (!gameEnded)
                DrawTitle();
            else
                DrawEnd();

            base.Draw(gameTime);
        }

        //Not used right now
        private void DrawEnd()
        {
            spriteBatch.Begin();
            spriteBatch.DrawString(font, "Congratulations! You beat all the levels in the game right now\nWant more? You can always create and share your own levels\nSee the readme in the levels folder for details \nTo exit, hit esc" , new Vector2(GraphicsDevice.Viewport.Width/2 - 200, GraphicsDevice.Viewport.Height/2 - 100), Color.Red);
            spriteBatch.End();

            resetFor3D();
        }

        private void DrawTitle()
        {
            spriteBatch.Begin();
            spriteBatch.Draw(titleScreen, new Rectangle(0,0,GraphicsDevice.Viewport.Width,GraphicsDevice.Viewport.Height), Color.White);
            spriteBatch.End();

            resetFor3D();
        }

        private void resetFor3D()
        {
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap; 
        }

        private void SetCurrentLevel(Level level)
        {
            float xPos = 0f;
            float yPos = 0f;
            float zPos = 0f;
            //player = new Player(Vector3.Zero);
            for (int y = 0; y < level.yBlocks; y++)
            {
                zPos = 0f;
                for (int z = 0; z < level.zBlocks; z++)
                {
                    xPos = 0f;
                    for (int x = 0; x < level.xBlocks; x++)
                    {
                        if (level.map[x, y, z] == 1)
                        {
                            blocks.Add(createBlock(level, new int[] {x, y, z}));
                        }
                        else if (level.map[x, y, z] == 2)
                        {
                            continue;
                        }
                        else if (level.map[x, y, z] == 9)
                        {
                            if (player != null)
                            {
                                player.Pos = new Vector3(xPos, yPos, zPos);
                                player.height = Block.BLOCK_SIZE * 2;
                                player.leftrightRot = 0;
                            }
                            else
                            {
                                player = new Player(new Vector3(xPos, yPos, zPos));
                            }
                        }
                     
                        xPos += level.blockSize;
                    }
                    zPos += level.blockSize;
                }
                yPos += level.blockSize;
            }

            Vector3 floorScale = new Vector3(level.xBlocks, 1, level.zBlocks) * level.blockSize;
            Block levelFloor = new Block(block, Vector3.Zero, Vector3.Zero, floorScale);
            levelFloor.Extragin = new Vector3(floorScale.X, 0, floorScale.Z);
            levelFloor.updateBounds();
            
            blocks.Add(levelFloor);
           
            camera.Follow(player);
        }

        private void DrawLevel()
        {
            foreach (Block block in blocks)
            {
                block.Draw(camera, "Colored");
            }

            spriteBatch.Begin();
            spriteBatch.DrawString(font, "Level " + currentLevel.id, new Vector2(0, 0), Color.Red);
            spriteBatch.End();

            //reset the device for 3d
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
        }

        private Level LoadLevelFromFile(String fileName)
        {
            Level returnLevel = new Level();
            if (File.Exists("levels\\" + fileName))
            {
                
                using (StreamReader sr = new StreamReader("levels\\" + fileName))
                {
                    String[] str = sr.ReadToEnd().Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
                    
                    try
                    {
                        returnLevel.blockSize = float.Parse(str[0]);

                        returnLevel.xBlocks = int.Parse(str[1]);
                        returnLevel.yBlocks = int.Parse(str[2]);
                        returnLevel.zBlocks = int.Parse(str[3]);

                        returnLevel.map = new int[returnLevel.xBlocks, returnLevel.yBlocks, returnLevel.zBlocks];

                        for (int y = 0; y < returnLevel.yBlocks; y++)
                            for (int z = 0; z < returnLevel.zBlocks; z++)
                                for (int x = 0; x < returnLevel.xBlocks; x++)
                                    returnLevel.map[x, y, z] = int.Parse(str[4 + (y * returnLevel.xBlocks * returnLevel.zBlocks) + ((z * returnLevel.xBlocks) + x)]);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Exception: " + e);
                        returnLevel.blockSize = -1; //mark level as invalid
                        return returnLevel;
                    }
                   
                    return returnLevel;
                }
            }
            else
            {
                Console.WriteLine("Cannot find file");
                returnLevel.blockSize = -1; //mark level as invalid
                return returnLevel;
            }
        }

        private Block createBlock(Level level, int[] position)
        {
            int maxX = findMaxX(level, position);
            int maxZ = findMaxZ(level, position);
            int maxY = 1;
            if (maxX > maxZ)
            {
                maxY = findMaxYForX(level, position, maxX);
                maxZ = 1;
            }
            else
            {
                maxY = findMaxYForZ(level, position, maxZ);
                maxX = 1;
            }
            
            Vector3 scale = new Vector3(maxX, maxY, maxZ);
            Block rtnVal = new Block(block, Vector3.Zero, Vector3.Zero, scale);
            rtnVal.Origin = new Vector3(position[0], position[1], position[2]) * level.blockSize;
            rtnVal.updateBounds();

            return rtnVal;
        }

        private int findMaxX(Level level, int[] position)
        {
            for (int i = position[0]; i < level.xBlocks; i++)
            {
                if (level.map[i, position[1], position[2]] != 1)
                    return i - position[0];
            }
            return level.xBlocks - position[0];
        }

        private int findMaxZ(Level level, int[] position)
        {
            for (int i = position[2]; i < level.zBlocks; i++)
            {
                if (level.map[position[0], position[1], i] != 1)
                    return i - position[2];
            }
            return level.zBlocks - position[2];
        }

        private int findMaxYForX(Level level, int[] position, int maxX)
        {
            for (int i = position[1]; i < level.yBlocks; i++)
            {
                for (int j = position[0]; j < maxX + position[0]; j++)
                {
                    if (level.map[j, i, position[2]] != 1)
                        return i - position[1];
                }
                //If this row could be filled, set it to 2's because it will be used.
                for (int k = position[0]; k < maxX + position[0]; k++)
                    level.map[k, i, position[2]] = 2;
            }
            return level.yBlocks - position[1];
        }

        private int findMaxYForZ(Level level, int[] position, int maxZ)
        {
            for (int i = position[1]; i < level.yBlocks; i++)
            {
                for (int j = position[2]; j < maxZ + position[2]; j++)
                {
                    if (level.map[position[0], i, j] != 1)
                        return i - position[1];
                }
                //If this row could be filled, set it to 2's because it will be used.
                for (int k = position[2]; k < maxZ + position[2]; k++)
                    level.map[position[0], i, k] = 2;
            }
            return level.yBlocks - position[1];
        }
    }
    
    public struct Level
    {
        public float blockSize;
        public int xBlocks, yBlocks, zBlocks, id;
        public int[,,] map;
    }
}
